// todo: 去除滚动条的依赖

function TextView(){
    this.init$Eventable();
    this.initRenderable();
    this.$textModel = new TextModel();
    this.$undoMgr = new UndoManager(this.$textModel);
    this.$fontMeasurer = new FontMeasurer();
    this.on(this.$fontMeasurer, 'FontChanged', this._onFontChanged);
    this.$fontInfo = null;
    this.$textLayout = new TextLayout(this.$textModel);
    this.$limitedRedraw = Utils.thinFunction(25, this.redraw);
    this.on(this.$textLayout, 'LayoutChanged', this._onTextLayoutChanged);
    this.$selection = new Selection(this);
    this.on(this.$selection, 'Changed', this._onSelectionChanged);
    this.on(this.$textModel, 'Changed', this._onTextChanged);
    this.$caret = new Caret();
    this.$textInput = new TextInput();
    this.on(this.$textInput, 'TextInput', this._onTextInput);
    this.on(this.$textInput, 'KeyDown', this._onKeyDown);
    this.$keyBindings = new KeyBindings(this, {
        'DELETE': 'delete',
        'BACKSPACE': 'backspace',
        'TAB': 'indent',
        'SHIFT+TAB': 'outdent',
        'HOME': 'nav_home',
        'END': 'nav_end',
        'LEFT': 'nav_left',
        'RIGHT': 'nav_right',
        'UP': 'nav_up',
        'DOWN': 'nav_down',
        'PAGE_UP': 'nav_page_up',
        'PAGE_DOWN': 'nav_page_down',
        'CTRL+A': 'selectall',
        'CTRL+Z': 'undo',
        'CTRL+Y': 'redo',
        'CTRL+C': 'copy',
        'CTRL+X': 'cut',
        'CTRL+V': 'paste'
    });
    this.$commands = new Commands(this);
    this.$textStyler = new TextStyler(this.$textModel);
    this.$scroller = new Scroller();
    this.on(this.$scroller, 'ClientSizeChanged', this._onClientSizeChanged);
    this.on(this.$scroller, 'Scroll', this._onScroll);
    this.$pageColumn = 128;
}

TextView.prototype = {
    _TPL_ELM: '<div id="#{id}" class="QE">' +
        '<div id="#{id}_gutter" onmousedown="return false;" ondragstart="return false;" class="QEGutter"></div>' +
        '<div id="#{id}_textView" class="QETextView">' +
        '#{measurerHTML}' +
        '#{scrollerHTML}' +
        '</div>' +
        '</div>',
    renderHTML: function (){
        return Utils.format(this._TPL_ELM, {
            id: this.id(),
            measurerHTML: this.$fontMeasurer.renderHTML(),
            scrollerHTML: this.$scroller.renderHTML()
        });
    },
    postRender: function (){
        this.$fontMeasurer.postRender();
        this.$fontInfo = this.$fontMeasurer.getFontInfo();
        this.$scroller.postRender();
        this.$selection.render(this.scrollElm());
        this.$caret.render(this.scrollElm());
        this.$textInput.render(this.scrollElm());
        this.on(this._contentElm(), 'mousedown', this._onMouseDown);
        this.on(this._contentElm(), 'mousewheel', this._onMouseWheel);
        this.redraw();
    },
    getTextModel: function (){
        return this.$textModel;
    },
    getTextLayout: function (){
        return this.$textLayout;
    },
    getText: function (startOffset, endOffset){
        return this.$textModel.getText(startOffset, endOffset);
    },
    setText: function (text, startOffset, endOffset){
        if (text.charAt(text.length - 1) !== '\r' ||
            text.charAt(text.length - 1) !== '\n') {
            text = text + '\n';
        }
        if (this.isRendered()) {
            this.$scroller.setScrollX(0);
            this.$scroller.setScrollY(0);
        }
        var offset = this.$textModel.setText(text, startOffset, endOffset);
        this.$selection.setOffset(offset);
        this.limitedRedraw();
        this.$undoMgr.save();
    },
    getSelection: function (){
        return this.$selection;
    },
    getCaret: function (){
        return this.$caret;
    },
    _getSize: function (){
        return [this.elm().offsetWidth, this.elm().offsetHeight];
    },
    scrollerElm: function (){
        return this.$scroller.scrollerElm();
    },
    scrollElm: function (){
        return this.$scroller.scrollElm();
    },
    _contentElm: function (){
        return this.$scroller.contentElm();
    },
    getColumnX: function (column){
        return column * this.$fontInfo.charWidth;
    },
    getRowY: function (row){
        return row * this.$fontInfo.lineHeight;
    },
    getContentWidth: function (){
        return this.$scroller.getContentWidth();
    },
    getContentHeight: function (){
        return this.$scroller.getContentHeight();
    },
    getLineHeight: function (){
        return this.$fontInfo.lineHeight;
    },
    scrollTo: function (x, y){
        this.scrollToX(x);
        this.scrollToY(y);
    },
    scrollToX: function (x){
        this.$scroller.setScrollX(x);
    },
    scrollToY: function (y){
        this.$scroller.setScrollY(y);
    },
    scrollToLine: function (lineIndex){
        this.scrollToRow(this.$textLayout.getRow(lineIndex));
    },
    scrollToRow: function (row){
        this.scrollToY(this.getRowY(row));
    },
    hitTest: function (point, absolute){
        if (absolute) {
            var rect = this._contentElm().getBoundingClientRect();
            point[0] += -rect.left + this.$scroller.getScrollX();
            point[1] += -rect.top + this.$scroller.getScrollY();
        }
        var textModel = this.$textModel;
        var fontInfo = this.$fontInfo;
        var row = Math.max(0, Math.min(textModel.getLineCount() - 1, Math.floor(point[1] / fontInfo.lineHeight)));
        var column = Math.max(0, Math.round(point[0] / fontInfo.charWidth));
        var r = this.$textLayout.getRowColumnOffset(row, column);
        return [r[0], row, r[1]];
    },
    getPosition: function (offset){
        var r = this.$textLayout.getRowAndColumn(offset);
        return [r[1] * this.$fontInfo.charWidth, r[0] * this.$fontInfo.lineHeight];
    },
    deferRedraw: function (notCaseCaret){
        if (this.limitedRedraw.timer) {
            return;
        }
        clearTimeout(this._redrawTimer);
        var this_ = this;
        this._redrawTimer = setTimeout(function (){
            this_.limitedRedraw(notCaseCaret);
        });
    },
    limitedRedraw: function (redrawByScroll){
        this.$limitedRedraw(redrawByScroll);
    },
    redraw: function (notCaseCaret){
        if (!this.isRendered()) {
            return;
        }
        if (!notCaseCaret) {
            this.ensureCaretVisible();
        }
        this._redrawGutter();
        var textViewElm = this.elm('textView');
        this.$scroller.setSize([
            textViewElm.offsetWidth,
            textViewElm.offsetHeight
        ]);
        this.$scroller.setContentSize([
            this.getColumnX(this.$textLayout.getMaxColumn() + 5),
            this.getRowY(this.$textLayout.getRowCount())
        ]);
        var charWidth = this.$fontInfo.charWidth;
        var pageColumn = this.$pageColumn;
        var pageWidth = pageColumn * charWidth;
        var lineHeight = this.$fontInfo.lineHeight;
        var scrollX = this.$scroller.getScrollX();
        var scrollY = this.$scroller.getScrollY();
        var clientWidth = this.$scroller.getClientWidth();
        var clientHeight = this.$scroller.getClientHeight();
        var startPage = Math.floor(scrollX / pageWidth);
        var endPage = Math.ceil((scrollX + clientWidth) / pageWidth);
        var startRow = ((scrollY / lineHeight) >> 2) << 2; // 对齐到4的倍数.
        var endRow = ((Math.ceil((scrollY + clientHeight) / lineHeight) >> 2) << 2) + 4; // 额外渲染四行
        endRow = Math.min(endRow, this.$textLayout.getRowCount());
        startRow = Math.min(Math.max(0, startRow), endRow);
        var buff = ['<div class="QETextContent" style="position:relative;left:', 0, 'px;top:', 0, 'px;">'];
        for (var page=startPage; page<endPage; page++) {
            buff.push('<div style="position:absolute;left:', (page - startPage) * pageWidth ,'px;top:0;">');
            var pageStartColumn = page * pageColumn;
            var pageEndColumn = pageStartColumn + pageColumn;
            for (var row=startRow; row<endRow; row++) {
                // todo: 这里可以优化一下.
                var r1 = this.$textLayout.getRowColumnOffset(row, pageStartColumn, -1);
                var r2 = this.$textLayout.getRowColumnOffset(row, pageEndColumn, 1);
                var startOffset = r1[0];
                var startColumn = r1[1];
                var endOffset = r2[0];
                var endColumn = r2[1];
                if (endColumn < pageStartColumn) {
                    buff.push('<div class="QELine" style="' +
//                        'height:', this.$fontInfo.lineHeight, 'px;',
//                        'padding-top: ', (this.$fontInfo.lineHeight - this.$fontInfo.charHeight - 2), 'px;',
                        '">\xa0</div>');
                } else {
                    buff.push('<div class="QELine"  style="' +
//                        'height:', this.$fontInfo.lineHeight, 'px;',
//                        'padding-top: ', (this.$fontInfo.lineHeight - this.$fontInfo.charHeight - 2), 'px;',
                        'margin-left:', (startColumn - pageStartColumn) * charWidth,'px;">');
                    this.renderInlineHTML(buff, startOffset, endOffset, this.$fontInfo, row);
                    buff.push('</div>');
                }
            }
            buff.push('</div>');
        }
        buff[1] = startPage * pageWidth - scrollX;
        buff[3] = this.getRowY(startRow) - scrollY;
        buff.push('</div>');
        this._contentElm().innerHTML = buff.join('');
    },
    _redrawGutter: function (){
        var charWidth = this.$fontInfo.charWidth;
        var lineHeight = this.$fontInfo.lineHeight;
        var scrollX = this.$scroller.getScrollX();
        var scrollY = this.$scroller.getScrollY();
        var clientWidth = this.$scroller.getClientWidth();
        var clientHeight = this.$scroller.getClientHeight();
        var startRow = ((scrollY / lineHeight) >> 2) << 2; // 对齐到4的倍数.
        var endRow = ((Math.ceil((scrollY + clientHeight) / lineHeight) >> 2) << 2) + 4; // 额外渲染四行
        endRow = Math.min(endRow, this.$textLayout.getRowCount());
        startRow = Math.min(Math.max(0, startRow), endRow);
        var buff = ['<div class="QELineNum" style="margin-top:', startRow * lineHeight - scrollY, 'px;">'];
        buff.push('<div class="QELine" style="height:0;padding:0;margin:0;overflow:hidden;">', this.$textLayout.getRowCount(), '</div>');
        for (var row=startRow;row<endRow;row++) {
            buff.push('<div class="QELine">', this.$textLayout.getRowLineIndex(row)[0] + 1,'</div>');
        }
        buff.push('</div>');
        this.elm('gutter').innerHTML = buff.join('');
    },
    renderInlineHTML: function (buff, startOffset, endOffset, fontInfo, row){
        var textStyler = this.$textStyler;
        var tokens = textStyler.getTokens(startOffset, endOffset);
        var offset = startOffset;
        var stepColumn = null;
        for (var i=0; i<tokens.length; i++) {
            var token = tokens[i];
            if (token.type) {
                buff.push('<span class="qed-', token.type ,'">');
                stepColumn = this.$textLayout.renderInlineHTML(buff, offset, offset + token.length, fontInfo, row, stepColumn);
                buff.push('</span>');
            } else {
                stepColumn = this.$textLayout.renderInlineHTML(buff, offset, offset + token.length, fontInfo, row, stepColumn);
            }
            offset += token.length;
        }
    },
    _onFontChanged: function (){
        this.$fontInfo = this.$fontMeasurer.getFontInfo();
    },
    _onClientSizeChanged: function (){
        this.deferRedraw(true);
    },
    _onScroll: function (){
        this.limitedRedraw(true);
    },
    _onMouseDown: function (evt){
        this.dispatchEvent({
            type: 'MouseDown',
            ctrlKey: evt.ctrlKey,
            altKey: evt.altKey,
            shiftKey: evt.shiftKey,
            clientX: evt.clientX,
            clientY: evt.clientY
        });
        this.$textInput.focus();
        DOMUtils.preventDefault(evt);
    },
    _onMouseWheel: function (evt){
        var delta = - DOMUtils.getWheelDelta(evt) * this.$fontInfo.lineHeight * 3;
        this.scrollToY(Math.max(0, Math.min(
            this.getContentHeight() - this.$scroller.getClientHeight(),
            this.$scroller.getScrollY() + delta)));
        DOMUtils.preventDefault(evt);
    },
    _onKeyDown: function (evt){
        this.$keyBindings.dispatchEvent(evt);
    },
    _onTextLayoutChanged: function (){
        // todo: 其实没有render之前连layout都不需要去做.
        if (!this.isRendered()) return;
        this.$scroller.setContentSize([
            this.getColumnX(this.$textLayout.getMaxColumn()),
            this.getRowY(this.$textLayout.getRowCount())
            ]);
        this.deferRedraw();
    },
    _onSelectionChanged: function (){
        if (this.isRendered()) {
            var offset = this.$selection.getFocusOffset();
            var point = this.getPosition(offset);
            this.$caret.setCaret(point, this.$fontInfo.lineHeight);
            this.$caret.restart();
            this.$textInput.setPosition(point);
        }
    },
    sendTextInput: function (text){
        var offset = this.$textModel.setText(text,
            this.$selection.getStartOffset(),
            this.$selection.getEndOffset());
        this.$selection.setOffset(offset);
        this.limitedRedraw();
    },
    ensureCaretVisible: function (){
        // todo:
        var pos = this.$caret.getPosition();
        if (pos[0] < this.$scroller.getScrollX()) {
            if (pos[0] < this.$scroller.getClientWidth()) {
                this.scrollToX(0);
            } else {
                this.scrollToX(pos[0] - 2);
            }
        } else if (pos[0] + 2 > this.$scroller.getScrollX() + this.$scroller.getClientWidth()) {
            this.scrollToX(pos[0] - this.$scroller.getClientWidth() + 2);
        }
        if (pos[1] < this.$scroller.getScrollY()) {
            this.scrollToY(pos[1]);
        } else if (pos[1] + this.$fontInfo.lineHeight > this.$scroller.getScrollY() + this.$scroller.getClientHeight()) {
            this.scrollToY(pos[1] + this.$fontInfo.lineHeight - this.$scroller.getClientHeight());
        }
    },
    ensureEndLine: function (){
        if (this.$textModel.getLineCount() === 0 ||
            this.$textModel.getLineLength(this.$textModel.getLineCount() - 1) !== 0) {
            this.$textModel.setText('\n',
                this.$textModel.getLength(),
                this.$textModel.getLength());
        }
    },
    _onTextInput: function (evt){
        this.sendTextInput(evt.text);
    },
    _onTextChanged: function (evt){
        if (this.isRendered()) {
//            var offset = evt.offset + evt.insertedText.length;
            this.$selection.setOffset(evt.offset);
//            this.ensureCaretVisible();
//            this.limitedRedraw();
        }
    },
    execCommand: function (cmd, p1, p2, p3){
        if (cmd === 'undo') {
            return this.$undoMgr.undo();
        } else if (cmd === 'redo') {
            return this.$undoMgr.redo();
        }
        this.$undoMgr.save();
        var result = this.$commands.execCommand(cmd, p1, p2, p3);
        this.$undoMgr.save();
        return result;
    }
};

Utils.extend(TextView, Eventable);
Utils.extend(TextView, Renderable);
